﻿//#define FORCE_CLOUD

using System;
using System.Web;
using System.Runtime.InteropServices;
using Microsoft.WindowsAzure.ServiceRuntime;
using Microsoft.WindowsAzure.StorageClient;
using Microsoft.WindowsAzure;
using System.Diagnostics;
using System.IO;
using System.Configuration;

namespace AzureFileCacheWeb
{

    #region CacheManager Code
    /// <summary>
    /// This class is used to populate local storage from Azure Storage.  It may be expanded later to include other functionality.
    /// </summary>
    public class LocalStorageCacheManager
    {
        public void Initialize()
        {
            CloudStorageAccount StorageAccount = default(CloudStorageAccount);

            if (RoleEnvironment.IsAvailable)
            {
                #region Setup CloudStorageAccount Configuration Setting Publisher
                CloudStorageAccount.SetConfigurationSettingPublisher((configName, configSetter) =>
                {
                    // for a console app, reading from App.config
                    configSetter(RoleEnvironment.GetConfigurationSettingValue(configName));
                });
                #endregion
#if FORCE_CLOUD
                //from role config if in cloud
                StorageAccount = CloudStorageAccount.FromConfigurationSetting("CloudStorageConnectionString");
#else
                if (IsDevFabric())
                {
                    StorageAccount = CloudStorageAccount.FromConfigurationSetting("LocalStorageConnectionString");
                }
                else
                {
                    StorageAccount = CloudStorageAccount.FromConfigurationSetting("CloudStorageConnectionString");
                }
#endif

            }

            CloudBlobClient client = StorageAccount.CreateCloudBlobClient();
            if (IsDevFabric())
            {
                //for dev storage have to pre-append "devstoreaccount1/" to the front of the container name
                PopulateLocalCache(StorageAccount, RoleEnvironment.GetConfigurationSettingValue("AzureStorageCacheContainer")
                    , RoleEnvironment.GetConfigurationSettingValue("LocalFileCache"));
            }
            else
            {
                PopulateLocalCache(StorageAccount, RoleEnvironment.GetConfigurationSettingValue("AzureStorageCacheContainer")
                    , RoleEnvironment.GetConfigurationSettingValue("LocalFileCache"));
            }


        }

        //check to see if we'er in the cloud or not;  this function can be compile to force the cloud
        private bool IsDevFabric()
        {
            bool retval = false;
#if FORCE_CLOUD
            retval = false;
#else
            retval = !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("_DF_STATE_DIRECTORY"));
#endif
            return retval;
        }

        //public void PopulateLocalCache(StorageCredentialsAccountAndKey StorageCreds, Uri BlobUri, Uri QueueUri, Uri TableUri)
        //{
        //    //create StorageAccount object pointit to the correct endpoints and providing credentials
        //    CloudStorageAccount StorageAccount = new CloudStorageAccount (StorageCreds, BlobUri, QueueUri, TableUri);

        //    if (IsDevFabric())
        //    {
        //        //for dev storage have to pre-append "devstoreaccount1/" to the front of the container name
        //        PopulateLocalCache(StorageAccount, "/"
        //            + RoleEnvironment.GetConfigurationSettingValue("AzureStorageCacheContainer")
        //            , RoleEnvironment.GetConfigurationSettingValue("LocalFileCache"));
        //    }
        //    else
        //    {
        //        PopulateLocalCache(StorageAccount, RoleEnvironment.GetConfigurationSettingValue("AzureStorageCacheContainer")
        //            , RoleEnvironment.GetConfigurationSettingValue("LocalFileCache"));
        //    }
        //}

        //this method is used to move files from Azure Storage to Local Storage
        public void PopulateLocalCache(CloudStorageAccount StorageAccount, string ContainerName, string LocalResourceName )
        {
            //remove double "/" chars
            ContainerName = ContainerName.Replace("//", "/");

            //create the blob client and use it to create the container object
            CloudBlobClient BlobClient = StorageAccount.CreateCloudBlobClient();

            //Note that here is where the container name is passed in order to get to the files we want
            CloudBlobContainer BlobContainer = new CloudBlobContainer(StorageAccount.BlobEndpoint.ToString() + "/" + ContainerName, BlobClient);

            //create the options needed to get the blob list
            BlobRequestOptions options = new BlobRequestOptions();
            options.AccessCondition = AccessCondition.None;
            options.BlobListingDetails = BlobListingDetails.All;
            options.UseFlatBlobListing = true;
            options.Timeout = new TimeSpan(0, 1, 0);

            //retrieve the list of files/blobs in the container.  There are ways to fetch by prefix, but in this instance we want them all
            System.Collections.Generic.IEnumerable<IListBlobItem> Blobs = BlobContainer.ListBlobs(options);
            


            //get local resource
            LocalResource CacheFolder = RoleEnvironment.GetLocalResource(LocalResourceName);

            Console.WriteLine("Name: " + CacheFolder.Name + "\n RootPath: " + CacheFolder.RootPath + "\n Max Size: " + CacheFolder.MaximumSizeInMegabytes.ToString());

            //iterate over the collect. grab the files and save them locally.
            foreach (IListBlobItem item in Blobs)
            {
                //evaluating in the debugger does not seem to work well, but console.writeline is fine
                Console.WriteLine(item.Uri.ToString());
                string path = CacheFolder.RootPath.ToLower();
                
                //named stored in the container with forward slashes "/"
                //need to escape them to save them to local storage

                string FileAbsPath = item.Uri.AbsolutePath.ToLower();

                //get rid of the container name
                //the the path of the URI is prefixed and postfixed with "/"; removing those too
                FileAbsPath = FileAbsPath.Remove(FileAbsPath.IndexOf("devstoreaccount1/"), "devstoreaccount1/".Length);                
                FileAbsPath = FileAbsPath.Replace("/" + ContainerName.ToLower() + "/", "");

                Microsoft.WindowsAzure.StorageClient.CloudPageBlob pageblob = new CloudPageBlob(item.Uri.ToString());

                string EncodedFileName = HttpUtility.UrlEncode(FileAbsPath);
                //byte[] blobbytes = pageblob.DownloadByteArray();

                pageblob.DownloadToFile(path + EncodedFileName, options);
            }

        }
    }
    #endregion

    #region CacheModule & support Code
    /// <summary>
    /// This class is the implementation for the HttpModule that handles resolving and serving cached content.
    /// </summary>
    public class LocalStorageCacheModule : IHttpModule
    {
        /// <summary>
        /// You will need to configure this module in the web.config file of your
        /// web and register it with IIS before being able to use it. For more information
        /// see the following link: http://go.microsoft.com/?linkid=8101007
        /// </summary>
        #region IHttpModule Members

        public void Dispose()
        {
            //clean-up code here.
        }

        public void Init(HttpApplication context)
        {
            context.BeginRequest += new EventHandler(context_BeginRequest);
        }

        protected bool MappedPath()
        {
            bool retval = false;
            //check request path against configured paths
            //currently is one setting in the webrole settings
            //TODO: create custom config to allow for multiple paths and file masks

            if (HttpContext.Current.Request.Url.ToString().IndexOf(RoleEnvironment.GetConfigurationSettingValue("MappedPath"),StringComparison.InvariantCultureIgnoreCase) > 0)
            {
                retval = true;
            }

            return retval;
        }

        void context_BeginRequest(object sender, EventArgs e)
        {
            //ensure that this is responding to the proper sender
            if (sender is HttpApplication)
            {
                //cast the object in order to use it
                HttpApplication context = sender as HttpApplication;
                
                //look for the URL indicator that you arbitrarily decided would be associated with content 
                //that you have saved in the webrole's local storage.  Note that you could have multiple 
                //storage locations and thus need to associate multiple URLs to each of their own storage
                //locations.
                string LocalFile = null;
                if (MappedPath())
                {
                    LocalFile = ResolveLocalFilePath(context.Request.Url);
                }
                //use lcase and look for the indicator
                if (LocalFile != null)
                {

                    string mimetype = MimeTypeUtil.CheckType(LocalFile);

                    //set the content type so that it is handled properly by the browser
                    context.Response.ContentType = mimetype;
                    context.Response.AddHeader("content-disposition", "filename=" + context.Request.Url.AbsolutePath);

                    //send the file back to the browser and end the response
                    context.Response.WriteFile(LocalFile);

                    context.Response.End();
                }
                else
                {
                    //migth want to do some logging here for unfound files
                }
            }
        }


        //NOTE: just standing up an indirection call here to prepare for when I refactor this code to
        //      be based on events and loosely loaded modules for matching rules

        //TODO: need to add code here to map requested path to configured LocalResource;
        //      must first add code elsewhere in the module and cachemanager to handle multiple LocalResources
        public string ResolveLocalFilePath(Uri RequestedPath)
        {
            string retval=null;

            //stripping off the first part of the path as that should represent the container name which we didn't store as part of the filename
            string FullFileName = RequestedPath.AbsolutePath.ToLower();
            FullFileName = FullFileName.Replace(RoleEnvironment.GetConfigurationSettingValue("MappedPath").ToLower() + "/", "");
            //this implementation uses UrlEncode paths
            FullFileName = HttpUtility.UrlEncode(FullFileName);

            //get the local storage folder as we need it to get the path to the resource
            LocalResource CacheFolder = RoleEnvironment.GetLocalResource(RoleEnvironment.GetConfigurationSettingValue("LocalFileCache"));
            FullFileName = CacheFolder.RootPath + FullFileName;

            //check for existence
            System.IO.FileInfo fi = new System.IO.FileInfo(FullFileName);
            if (fi.Exists)
            {
                retval = fi.FullName;
            }


            return retval;
        }

        #endregion

        public void OnLogRequest(Object source, EventArgs e)
        {
            //custom logging logic can go here
        }
    }


    //Thanks to Ben Waldron for this class
    public class MimeTypeUtil
    {
        [DllImport(@"urlmon.dll", CharSet = CharSet.Auto)]
        private extern static System.UInt32 FindMimeFromData(
             System.UInt32 pBC,
             [MarshalAs(UnmanagedType.LPStr)] System.String pwzUrl,
             [MarshalAs(UnmanagedType.LPArray)] byte[] pBuffer,
             System.UInt32 cbSize,
             [MarshalAs(UnmanagedType.LPStr)] System.String pwzMimeProposed,
             System.UInt32 dwMimeFlags,
             out System.UInt32 ppwzMimeOut,
             System.UInt32 dwReserved);

        public static string CheckType(string filePath)
        {
            byte[] buffer = new byte[256];
            // grab the first 256 bytes on the file
            using (FileStream fileStream = new FileStream(filePath, FileMode.Open))
            {
                if (fileStream.Length >= 256)
                    fileStream.Read(buffer, 0, 256);
                else
                    fileStream.Read(buffer, 0, (int)fileStream.Length);
            }
            try
            {
                System.UInt32 mimeType;
                System.UInt32 returnValue = FindMimeFromData(0, null, buffer, 256, null, 0, out mimeType, 0);
                System.IntPtr mimeTypePointer = new IntPtr(mimeType);
                return Marshal.PtrToStringUni(mimeTypePointer);
            }
            catch (Exception ex)
            {
                return ex.Message;
            }
        }
    #endregion

    }



    
}
